/**
 * 
 */
package gpl.scotlandyard.services;

import gpl.scotlandyard.beans.MarkedNode;
import gpl.scotlandyard.beans.basics.Link;
import gpl.scotlandyard.beans.basics.Node;
import gpl.scotlandyard.beans.basics.Player;
import gpl.scotlandyard.beans.basics.Ticket;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import org.apache.log4j.Logger;

/** @author Norbert */
public class MoveTracker extends Player.ListenerImpl {

  private static final Logger LOG = Logger.getLogger(MoveTracker.class);

  private static final int SORT_NUMBER = 0;

  private static final int SORT_MARK = 1;

  private final List<MoveTracker.Listener> listeners = new ArrayList<MoveTracker.Listener>();

  private final Player trackedPlayer;

  private List<MarkedNode> possiblePositions = new ArrayList<MarkedNode>();

  private int sortMode;

  private final LinkedList<List<MarkedNode>> stackPossiblePositions = new LinkedList<List<MarkedNode>>();

  /** Ceates a tracker on this player. By default list is sorted on marks. */
  public MoveTracker(Player player) {
    this.trackedPlayer = player;
    trackedPlayer.addListener(this);
    sortMode = SORT_MARK;
  }

  /** @param ticket */
  public void movesWithTicket(Ticket ticket) {
    saveCurrentPossibilities();
    // search all possible
    ArrayList<MarkedNode> futurePositions = new ArrayList<MarkedNode>();
    for (MarkedNode srcNode : possiblePositions) {
      for (Link nodeLink : srcNode.getLinks()) {
        if (nodeLink.getTicket() == ticket || ticket == Ticket.HIDE) {
          MarkedNode nextNode = new MarkedNode(nodeLink.getToNode());
          nextNode.setMark(srcNode.getMark());
          if (!futurePositions.contains(nextNode)) {
            futurePositions.add(nextNode);
            LOG.debug("future node added : " + nextNode);
          } else { // if it's already a future possible node : choose better mark
            MarkedNode existing = null;
            for (MarkedNode mNode : futurePositions) {
              if (mNode.equals(nextNode)) {
                existing = mNode;
                break;
              }
            }
            if (existing.getMark() < nextNode.getMark()) {
              futurePositions.remove(existing);
              futurePositions.add(nextNode);
              LOG.debug("future node corrected with better color : " + nextNode);
            }
          }
        }
      }
    }
    possiblePositions = futurePositions;
    fireUpdated();
  }

  /** Sorts the list by node number. */
  public void sortByNumber() {
    sortMode = SORT_NUMBER;
    fireUpdated();
  }

  /** Sorts the list by node mark and number. */
  public void sortByMark() {
    sortMode = SORT_MARK;
    fireUpdated();
  }

  private void sortList() {
    switch (sortMode) {
      case SORT_NUMBER:
        Collections.sort(possiblePositions, MarkedNode.getNumberComparator());
        break;
      case SORT_MARK:
        Collections.sort(possiblePositions, MarkedNode.getMarkNumberComparator());
        break;
      default:
        throw new IllegalStateException("all modes are clearly treated");
    }
  }

  /** Saves possibilities if not empty. Avoids first empty list. */
  private void saveCurrentPossibilities() {
    if (!possiblePositions.isEmpty()) {
      stackPossiblePositions.add(possiblePositions);
    }
  }

  /** @return unmodifiable list of possible positions. */
  public List<MarkedNode> getPossiblePositions() {
    return Collections.unmodifiableList(possiblePositions);
  }

  /**
   * @param ticket
   * @return true if tracker player could move with this ticket.
   */
  public boolean couldMoveWith(Ticket ticket) {
    if (ticket == Ticket.HIDE) {
      return possiblePositions.size() > 0;
    }
    for (MarkedNode node : possiblePositions) {
      for (Link link : node.getLinks()) {
        if (link.getTicket() == ticket) {
          return true;
        }
      }
    }
    return false;
  }

  /** @param node */
  public void setKnownPosition(Node node) {
    saveCurrentPossibilities();
    possiblePositions.clear();
    LOG.info(trackedPlayer.getName() + " known position " + node.getNumber());
    possiblePositions.add(new MarkedNode(node));
    fireUpdated();
  }

  /** @param node */
  public void setKnownImpossiblePosition(Node node) {
    if (possiblePositions.remove(new MarkedNode(node))) {
      LOG.info(trackedPlayer.getName() + " remove known impossible node " + node.getNumber());
      fireUpdated();
    } else {
      LOG.warn(trackedPlayer.getName() + " can't remove known impossible node " + node.getNumber());
    }
  }

  /**
   * @param position
   * @param mark
   */
  public void changeMark(int position, int mark) {
    if (mark == MarkedNode.IMPOSSIBLE_MARK) {
      setKnownImpossiblePosition(possiblePositions.get(position).getNode());
    } else {
      possiblePositions.get(position).setMark(mark);
    }
    fireUpdated();
  }

  /** @param possible */
  public void setPossiblePositions(List<Node> possible) {
    saveCurrentPossibilities();
    possiblePositions.clear();
    for (Node node : possible) {
      possiblePositions.add(new MarkedNode(node));
    }
    fireUpdated();
  }

  /**
   * 
   */
  public void clearMarks() {
    for (MarkedNode mn : possiblePositions) {
      mn.setMark(MarkedNode.NEUTRAL_MARK);
    }
    fireUpdated();
  }

  /** Restores the last possible positions in the stack. Current list is lost. */
  public boolean rollback() {
    if (stackPossiblePositions.isEmpty()) {
      return false;
    }
    possiblePositions = stackPossiblePositions.removeLast();
    fireUpdated();
    return true;
  }

  /** @return */
  public int getStackSize() {
    return stackPossiblePositions.size();
  }

  @Override
  public void playerTicketsUpdated(Player player) {
    // do nothing for the moment
  }

  @Override
  public void playerMoved(Player player, Link link) {
    if (!Node.getUnknownNode().equals(link.getToNode())) {
      setKnownPosition(link.getToNode());
    } else {
      movesWithTicket(link.getTicket());
    }
  }

  /** @return trackedPlayer */
  public Player getPlayer() {
    return trackedPlayer;
  }

  /** @param listener */
  public void addListener(MoveTracker.Listener listener) {
    listeners.add(listener);
  }

  /** @param listener */
  public void removeListener(MoveTracker.Listener listener) {
    listeners.remove(listener);
  }

  /**
   * 
   */
  private void fireUpdated() {
    sortList();
    for (MoveTracker.Listener listener : listeners) {
      listener.updated();
    }
  }

  /** @author norbert */
  public interface Listener {
    /**
     * 
     */
    void updated();
  }

  /** @author norbert */
  public static class ListenerImpl implements Listener {

    @Override
    public void updated() {
      // does nothing
    }
  }

}
